/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.util.xml;

import java.io.BufferedReader;
import java.io.CharConversionException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

/**
 * 检测XML流是使用DTD还是XSD验证
 * Detects whether an XML stream is using DTD- or XSD-based validation.
 *
 * @author Rob Harrop
 * @author Juergen Hoeller
 * @since 2.0
 */
public class XmlValidationModeDetector {

	/**
	 * 禁用验证
	 * Indicates that the validation should be disabled.
	 */
	public static final int VALIDATION_NONE = 0;

	/**
	 * 因为不能够找到明确的配置,所以会自动匹配验证模式.
	 * Indicates that the validation mode should be auto-guessed, since we cannot find
	 * a clear indication (probably choked on some special characters, or the like).
	 */
	public static final int VALIDATION_AUTO = 1;

	/**
	 * 使用DTD验证模式
	 * Indicates that DTD validation should be used (we found a "DOCTYPE" declaration).
	 */
	public static final int VALIDATION_DTD = 2;

	/**
	 * 使用XSD验证模式
	 * Indicates that XSD validation should be used (found no "DOCTYPE" declaration).
	 */
	public static final int VALIDATION_XSD = 3;


	/**
	 * 在XML文档里面,该标记表名正在使用DTD验证模式.
	 * The token in a XML document that declares the DTD to use for validation
	 * and thus that DTD validation is being used.
	 */
	private static final String DOCTYPE = "DOCTYPE";

	/**
	 * XML中注释开始的标记
	 * The token that indicates the start of an XML comment.
	 */
	private static final String START_COMMENT = "<!--";

	/**
	 * XML中注释结束的标记.
	 * The token that indicates the end of an XML comment.
	 */
	private static final String END_COMMENT = "-->";


	/**
	 * 表名当前的分析位置是否在XML注释中.
	 * Indicates whether or not the current parse position is inside an XML comment.
	 */
	private boolean inComment;


	/**
	 * 在指定输入流中验证XML文档的验证模式.
	 * 需要注意的是,输入流将会在方法返回之前被关闭.
	 * <p>
	 * Detect the validation mode for the XML document in the supplied {@link InputStream}.
	 * Note that the supplied {@link InputStream} is closed by this method before returning.
	 *
	 * @param inputStream the InputStream to parse
	 * @throws IOException in case of I/O failure
	 * @see #VALIDATION_DTD
	 * @see #VALIDATION_XSD
	 */
	public int detectValidationMode(InputStream inputStream) throws IOException {
		//浏览文件并查找 DOCTYPE.
		// Peek into the file to look for DOCTYPE.
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		try {
			//声明是DTD验证模式的变量,默认不是.
			boolean isDtdValidated = false;
			//按行读取数据.
			String content;
			while ((content = reader.readLine()) != null) {
				content = consumeCommentTokens(content);
				if (this.inComment || !StringUtils.hasText(content)) {
					//如果字符串没有内容,则跳过
					continue;
				}
				if (hasDoctype(content)) {
					//如果内容中包含了DTD验证模式的DOCTYPE,设置为DTD验证模式,并跳出方法(读取文件内容的方法也会在读取到DOCTYPE之后退出,
					// 所以此处会终止该方法).
					isDtdValidated = true;
					break;
				}
				if (hasOpeningTag(content)) {
					//这里面是一些非注释且不包含DOCTYPE的内容,在此处不需要处理.
					// End of meaningful data...
					break;
				}
			}
			//这里要么是DTD要么是XSD.
			return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
		} catch (CharConversionException ex) {
			// Choked on some character encoding...
			// Leave the decision up to the caller.
			//Ok,你竟然敢抛出异常,那就自动匹配验证模式吧.
			return VALIDATION_AUTO;
		} finally {
			//关闭流
			reader.close();
		}
	}


	/**
	 * Does the content contain the DTD DOCTYPE declaration?
	 */
	private boolean hasDoctype(String content) {
		return content.contains(DOCTYPE);
	}

	/**
	 * Does the supplied content contain an XML opening tag. If the parse state is currently
	 * in an XML comment then this method always returns false. It is expected that all comment
	 * tokens will have consumed for the supplied content before passing the remainder to this method.
	 */
	private boolean hasOpeningTag(String content) {
		if (this.inComment) {
			return false;
		}
		int openTagIndex = content.indexOf('<');
		return (openTagIndex > -1 && (content.length() > openTagIndex + 1) &&
				Character.isLetter(content.charAt(openTagIndex + 1)));
	}

	/**
	 * 删除给定字符串里面的注释内容,并返回其余内容,因为字符串可能都是注释,所以有可能会返回空.
	 * 不过,对于当前方法来说,其目的只是为了寻找DOCTYPE,所以在找到DOCTYPE之后,方法就会直接返回.
	 * Consumes all the leading comment data in the given String and returns the remaining content, which
	 * may be empty since the supplied content might be all comment data. For our purposes it is only important
	 * to strip leading comment content on a line since the first piece of non comment content will be either
	 * the DOCTYPE declaration or the root element of the document.
	 */
	@Nullable
	private String consumeCommentTokens(String line) {
		if (!line.contains(START_COMMENT) && !line.contains(END_COMMENT)) {
			//如果内容中不含有注释的开始标志和结束标志,则表明当前不是注释内容,因为当读取到注释开始标志之后,下面的操作会消耗掉整个注释内容.直到
			// 读取到非注释内容或者文档末尾.
			// 具体的逻辑是在consume方法中会处理掉所有的注释内容.
			return line;
		}
		//当前内容包含了注释开头标志或者注释结束标志,则开始处理注释的内容.
		String currLine = line;
		while ((currLine = consume(currLine)) != null) {
			if (!this.inComment && !currLine.trim().startsWith(START_COMMENT)) {
				//这里的while就表示一直读取到包含注释结尾标志的内容
				//如果不在注释内,同时也不是注释开始标志,那就表示是文档结束标志.
				return currLine;
			}
		}
		return null;
	}

	/**
	 * 读取内容一直到获取到下一个注释标志.并更新inComment标志.同时返回其余的文本.
	 * Consume the next comment token, update the "inComment" flag
	 * and return the remaining content.
	 */
	@Nullable
	private String consume(String line) {
		//如果当前在注释内的话,角标就是注释后的内容,如果是不在注释内的话,就取开始前的内容.
		//反正目的就是返回非注释内容.
		int index = (this.inComment ? endComment(line) : startComment(line));//注释标志所在行中的位置
		return (index == -1 ? null : line.substring(index));
	}

	/**
	 * 查找内容里注释开始标志的位置
	 * Try to consume the {@link #START_COMMENT} token.
	 *
	 * @see #commentToken(String, String, boolean)
	 */
	private int startComment(String line) {
		return commentToken(line, START_COMMENT, true);
	}

	/**
	 * @param line
	 * @return
	 */
	private int endComment(String line) {
		return commentToken(line, END_COMMENT, false);
	}

	/**
	 * 尝试消费给定的内容里的标志,同时将注释解析状态修改为给定的状态.
	 * 同时返回标志在内容里的位置,该位置不包含标志内容,如果没有找到标志的话返回-1.
	 * <p>
	 * <p>
	 * Try to consume the supplied token against the supplied content and update the
	 * in comment parse state to the supplied value. Returns the index into the content
	 * which is after the token or -1 if the token is not found.
	 */
	private int commentToken(String line, String token, boolean inCommentIfPresent) {
		int index = line.indexOf(token);
		if (index > -1) {
			//包含该元素,更新状态
			this.inComment = inCommentIfPresent;
		}
		return (index == -1 ? index : index + token.length());//不存在返回-1,存在返回去除内容的位置.
	}

}
